import pandas as pd
import numpy as np
import math
import pickle
from sklearn.model_selection import train_test_split
import dalex as dx
import lime
df_wines = pd.read_csv('winequality-red.csv')
df_wines["is_good"] = df_wines.apply(lambda row: 1 if row.quality > 5 else 0, axis = 1)
X = df_wines.drop(["is_good", 'quality'], axis=1)
y = df_wines["is_good"]
X_train, X_test, y_train, y_test = train_test_split(X, y, stratify=y,random_state = 42)
y_test = y_test.reset_index()["is_good"]
xgb = pickle.load(open("xgb", 'rb'))
y_pred = xgb.predict(X_test)
print("Accuracy: ", sum(y_test == y_pred)/y_test.shape[0]*100, "%", sep = "")
Accuracy: 78.5%
explainer = dx.Explainer(xgb, X_train, y_train, label = "Random Forest")
Preparation of a new explainer is initiated -> data : 1199 rows 11 cols -> target variable : Parameter 'y' was a pandas.Series. Converted to a numpy.ndarray. -> target variable : 1199 values -> model_class : sklearn.model_selection._search.RandomizedSearchCV (default) -> label : Random Forest -> predict function : <function yhat_proba_default at 0x000001C91BF64AF0> will be used (default) -> predict function : Accepts pandas.DataFrame and numpy.ndarray. -> predicted values : min = 0.00364, mean = 0.535, max = 1.0 -> model type : classification will be used (default) -> residual function : difference between y and yhat (default) -> residuals : min = -0.626, mean = -2.74e-05, max = 0.775 -> model_info : package sklearn A new explainer has been created!
X_test.iloc[42,:]
fixed acidity 7.1000 volatile acidity 0.5900 citric acid 0.0100 residual sugar 2.3000 chlorides 0.0800 free sulfur dioxide 27.0000 total sulfur dioxide 43.0000 density 0.9955 pH 3.4200 sulphates 0.5800 alcohol 10.7000 Name: 895, dtype: float64
explainer.predict(X_test)[42]
0.82479346
y_test[42]
1
Wylosowana obserwacja o indeksie 42 wydaje się być całkiem standardowa - model na 82% przewidywał dobrą jakość wina... i miał rację.
cp = explainer.predict_profile(X_test.iloc[42,:])
Calculating ceteris paribus: 100%|████████████████████████████████████████████████████| 11/11 [00:00<00:00, 188.46it/s]
cp.plot()
🍷 Powyższe wykresy generalnie zdają się być raczej chaotyczne i Ceteris Paribus na pierwsze parę rzutów oka zaskakuje.
🍷 Interesująca zdaje się już pierwsza wizualizacja - kwasowość stała dla wartości większych niż 7.1 w przypadku wiersza 42. zawsze delikatnie skacze wokół 80% szans na wino jakościowe, przy czym robi to dosyć nieregularnie. Przy mniejszej wartości zmiennej szansa na pozytywną predykcję oceny wina byłaby nieco mniejsza, ale nieznacznie. Tak czy inaczej nie jest to kolumna, której modyfikacja wartości dla tego konkretnego przypadku dużo by zmieniła.
🍷 Bardziej typową sytuację widzimy dla kwasowości zmiennej - tu wraz ze wzrostem ocena by malała; przy czym dla wartości od ok. 1 predykcja owocowałaby minimalnie we wskazanie wina złego wg. ekspertów. Największy spadek można zaobserwować około 0.85 acidity - spadek predykcji ~20% na odcinku parunastu setnych współczynnika! Interesujące, że po osiągnięciu pułapu ~1.1 predykcja zatrzymuje się na stałym poziomie 0.462.
🍷 Kwas cytrynowy jeszcze ciekawszy! Tu widzimy regularny spadek od naszej wartości bliskiej zera aż do 0.6... a potem ni stąd, ni zowąd - nagły ogromny wzrost. W tym konkretnym wypadku największą predykcją otrzymujemy zarówno dla wartości bardzo dużych, jak i bardzo małych (!!).
🍷 Wreszcie nieodfermentowany cukier póki co najmniej ciekawy. Jest to kolumna niemalże niemająca wpływu na wynik predykcji obserwacji. Obserwujemy tu niemalże bez zmian tę samą wartość, która dla większości potencjalnych wartości zostaje taka sama lub nieznacznie maleje, za wyjątkiem wzrostu na około ósemce i turbulencjach przy stosunkowo najmniejszych wartościach.
🍷 Dla siarczanów mamy niemalże wszystko w pakiecie! Odcinek stały, część gwałtownych skoków, parę bardziej regularnych zależności miejscami... do wyboru, do koloru. Tak czy inaczej także i tu nie udało by nam się tak zmanipulować, aby przez modyfikację pojedynczej kolumny uzyskać w efekcie
🍷 Wartości funkcji cząsteczek dwutlenku siarki niezwiązanych z innymi cząsteczkami cechują się bardzo małą wariancją pod względme nieregularności - można stwierdzić: najmniej znacząca zmienna w tym zestawie.
🍷 A całkowita zawartość dwutlenku siarki już baardzo ciekawa! Dla wartości 94 otrzymalibyśmy absolutnie pozytywną predykcję, zaś dla pozornie nie wiele większej - 111 - już absolutnie negatywną. W kontraście do kolumny analizowanej powyżej, tu mamy bezkonkurencyjnie najbardziej wpływową na predykcję - przynajmniej w kontekście manipulacji wartościami. Różnica wyniku największa ze wszystkich i co ciekawsze - mniej-więcej w połowie stawki analizowanych wartości.
🍷 Gęstość dosyć typowa. Zgodnie z intuicją z macierzą korelacji z targetem większa wartość systematycznie rezultuje w mniejszą szansę na pozytywny wynik, ale jak to nasz nieidealny XGBoost wraz z ekspertami, którzy są tylko ludźmi, zrobili - jest trochę nieregularności.
🍷 Odczyn roztworu pH? Właściwie patrz wyżej. Dosyć spodziewany rezultat, wpływ zaś na poziomie średnim. Tu dodatkowo mamy niestandardowy spadek na poziomie wartości ok. 3.6 - może nie tak spektakularny jak przy total sulfur dioxide, ale wciąż.
🍷 Zawartość siarczanów odpowiedzialnych za konserwowanie wina bardzo ładna - taka trochę krzywa ROC wyszła (: No, może za wyjątkiem spadku na około 0.84 wartości i paru innych mniej widocznych momentów. Raczej systematyczny wzrost, przy czym dosyć gwałtowny na początku, potem już coraz lżejszy i mniej śmiały. Trzeba by stosunkowo baardzo mało owych siarczanów, żeby model stwierdził, że eksperci negatywnie ocenią wino.
🍷 Mimo największej korelacji z targetem, nie mamy tu wcale takiego 100% pewnego wzrostu! Owszem, wraz z większym procentem alkoholu predykcja
🍷 Biorąc w całość: Ceteris Paribus daje radę i w efektywny [a także efektowny] sposób pokazuje naturę zmiennych i wpływ turbulencji na nie poszczególne na predykcję modelu. Zachwycające jest to, jak w każdym konkretnym wypadku został wygenerowany zupełnie inny wykres. Póki co subiektywnie moja ulubiona metoda poznana na tym przedmiocie!!
explainer.predict(X_test)[44]
0.07238567
y_test[44]
0
cp2 = explainer.predict_profile(X_test.iloc[44,:])
Calculating ceteris paribus: 100%|████████████████████████████████████████████████████| 11/11 [00:00<00:00, 186.94it/s]
cp2.plot()
🍷 W kontraście do id=42 to mamy styczność z winem bezdyskusyjnie złym - i to zarówno w opinii modelu, jak i ekspertów.
🍷 Widać to także na wykresach - w większości przypadków niezależnie od modyfikacji wartości, wszystkie predykcje oscylują wokół zera.
🍷 Różnicę widać chociażby patrząc na zawartość alkoholu - dla poprzedniego przykładu niezależnie od niej, wino i tak byłoby ocenione jako dobre [biorąc pod uwagę inne czynniki]. Tu zaś, dla pewnych wartości (między ok. 11.5 a 13% - co ciekawe taki sam przedział, dla którego id=42 byłoby niemalże na 100% wypredykowane jako pozytywne), manipulując tylko tą kolumną - końcowy binarny rezultat by się odwrócił.
🍷 Z drugiej strony zmiana całkowitej zawartości dwutlenku siarki, która dla pierwszej analizowanej obserwacji mogłaby mocno wpłynąć ne wynik predykcji - tu tak jak i większość rozpatrywanych kolumn, jako zmieniana jedna jedyna właściwie nie ma znaczenia.